#---------------------------------------------------------
# pxbcore.py -Python XML Beans
# 
# david connell
# 11/23/02
#---------------------------------------------------------
#            Change Log
#
# 03/15/05 - dkc
#  - Moved psbServer into its own module (pxbdb)
#---------------------------------------------------------

"""
<b><u>Intro</u></b>

A Python module that provides the infra-structure
for XML based 'Beans'.  The pxb class wrapps a dom
node and presents a set of standard 'services'.  This 
class may be subclassed and enhanced with a 'business
language' API that sits on top of the pxb wrapper.

Other interesting features include the pxbServer 
(moved to the pxbdb module), pxbQuery and pxbResultSet 
which bring in a bit of 'db-ness' to the party.

<b><u>Classes</u></b>

pxb - a wrapper for an XML node which represents an
XML document or a subset of such a document.  If the
pxb is pointing to a root node it is referred to as 
a 'document pxb'.  A document pxb can be queried to 
produce a result set containing a pxb list (See below).
pxbs hold a pointer to their parent document pxb so
that updates can be persisted.  
  
pxbQuery - can be set up for XPath or node name/attribute 
match modes.  When executed, it returns a list of pxbs  
with matching nodes as a pxbResultSet (keep reading...) 

pxbResultSet - provides a (thin) wrapper around the pxb
list returned by a pxbQuery.

pxbServer - houses a set of pxb documents available (via API)
for reuse within an interpreter.  (This class was moved to the
pxbdb module in this package {dkc: 03/15/05})
"""
# 
# -- Import XML Support Classes --
#
from lxml import etree

#
# -- Python Lib Support --
#

import sys
import urllib2
from datetime import datetime 
now = datetime.now

DEBUG_FLAG = 0

_xslt = True
    
try:
    from cStringIO import StringIO
except:
    from StringIO import StringIO

from pspy.gear import Notification
#
# -- PXB Query Class --
#
class pxbQuery:
    """Utility class for pxb selects"""
    
    def __init__(self, nodeName='', attributes={}):
        """Set up member variables"""
        self._nodeName = nodeName
        self._attributes = attributes
        self._pxb = None
        
    def _setpxb(self, pxb):
        """Allow the query to self execute"""    
        rv = self._pxb        
        self._pxb = pxb

        return rv
        
    def canExecute(self):
        """See if we have an onboard PXB"""    
        rv = 1
        if self._pxb == None:
            rv = 0
        return rv
        
    def execute(self):
        """Run the query against the internal PXB"""
        return self._pxb.select(self)
        
    def setAttribute(self, name, value):
        """Add attribute to query"""
        self._attributes[name] = value

    def getAttribute(self, name):
        """return value of specific attribute"""
        return self._attributes[name]
        
    def setAttributes(self, attrs={}):
        """Set/clear the query attribute criteria"""
        self._attributes = attrs
        
    def getAttributes(self):
        """Return entire collection of attributes"""
        return self._attributes
        
    def clearAttributes(self):
        """Refresh the internal attributes dictionary"""
        self._attributes = {}
        
    def    setNodeName(self, nodeName):
        """Set name of nodes to query or
        an xpath to query the DOM with
        """
        self._nodeName = nodeName
        
    def getNodeName(self):
        """Retrieve node name."""
        return self._nodeName

#
# -- PXB ResultSet --
#
class pxbResultSet:
    """Wrapper for nodelist returned from pxbQuery execution"""
    
    def __init__(self):
        """Constructor"""
        self._nodeList = []
        self._ndx = -1
        self._EOF = 1
        
    def rewind(self):
        """If we have a list, go back to start"""
        if len(self._nodeList) > 0:
            self._ndx = -1
            self.EOF = 0
            
    def getCount(self):
        """Return the number of results"""
        return len(self._nodeList)        
            
    def getFirst(self):        
        """Retrieve the first record in set"""
        self.rewind()
        return self.getNext()
        
    def hasNext(self):
        """see if we're at end of queue and return 1 if
        there are more nodes, 0 if not"""
        rv = 0
        
        if (self._ndx + 1) < len(self._nodeList):
            rv = 1
        
        return rv
    
    def getNext(self):
        """Retrieve next node in list or None if EOF"""
        rs = None
        
        # range check
        if self._ndx >= (len(self._nodeList) - 1):
            raise Exception("pxbResultSet <Error>: EOF Reached")    

        # bump the index
        self._ndx += 1

        # set the return value
        rs = self._nodeList[self._ndx]

        # check for EOF
        if self._ndx + 1 == len(self._nodeList):
            self._EOF = 1

        # and send back the loot
        return rs         
        
    def getNodeList(self):
        """Return the result list"""
        return self._nodeList
    
    def setNodeList(self, nodes=[]):
        """Replace internal result set"""
        self._nodeList = nodes
        if len(nodes) > 0:
            self._EOF = 0
        else:
            self._EOF = 1

        self._ndx = -1

    def addNode(self, node):
        """Add a node to the result set"""
        self._nodeList.append(node)        
        self._EOF = 0
        
    def append(self, rs):
        'Append another result set to this one'
        while rs.hasNext():
            self.addNode(rs.getNext())
            
            
# -- Python XML Bean Class --
#
class pxb:
    """DOM Node Wrapper"""

    def __init__(self, xmlFile=None):
        """Default Constructor"""        
        # 4Suite xslt availability flag
        global _xslt

        self._node = None
        self._dom = None
        self._fileName = ''
        self._setup()
        self._xform = _xslt

        if xmlFile is not None:
            self.load(xmlFile)
            
    @staticmethod
    def parse(xml):
        "parse xml and return pxb"
        xio = StringIO(xml)
        p = pxb().load(xio)
        return p
    
    # add some magic
    def __str__(self):
        return self.toString()
    
    # 
    # -- Class Services --
    #
    def _setup(self):
        """Reset dom/node relations and release pointers"""
        # if we have a dom and no node, set it to entire tree
        if self._dom != None and self._node == None:
            self._node = self._dom.getroot()

            # we now have a valid pxb (or so we think...)
            self._validity = 1

        # reset pointers
        self._currNode = self._node

    def _getNodesByName(self, nodeName, context=None):
        """Retrieve node list of all sub nodes of context
        (or current node) that are named 'nodeName'.  nodeName
        may either be a simple node name or an xpath"""
        PXB_XPATH_CHARS = "./"
        
        # node name starts with xpath chars,
        if nodeName[0] in PXB_XPATH_CHARS:
            # just use it 
            xp = nodeName
        else:          
            # build the default xpath as all 
            # matched nodes under current node
            xp = ".//%s" % nodeName
        
        # sys.stderr.write("xpath=%s\n" % xp)
        
        # set the proper context
        if context == None:
            context = self._currNode
        
        # and return the resulting list
        return context.ETXPath(xp)
        
    def _extractNodeToDict(self, node=None, dct=None, recurse=0):
        """fills or appends the text data from a node's
        child elements into a dictionary.
        """
        # default node to current node
        if node == None:
            node = self._currNode or self._node
            
        # defaults to new directory
        if dct == None:
            dct = {}
            
        # walk the children list extracting data
        for ch in node:
            # check to see if this is an element
            if etree.iselement(node):
                # if there is a text node
                if len(ch.text):
                    #and extract the data to the dict
                    dct[ch.tag] = str(ch.text)
                else:
                    if recurse != 0:

                        dct = self._extractNodeToDict(ch, dict)
                                    
        # return the results
        return dct

    def _extractAttrsToDict(self, node=None):
        """
        Create a map of attributes for the current node.
        """
        if not node:
            node = self._currNode
                
        return node.attrib
        
    def _makePXB(self, node):
        """Create a new pxb from a node"""
        p = pxb()
        p.setNode(node, self._dom)
        p.setFileName(self._fileName)

        return p

    def _setupInputSource(self,fn,fo):
        """
        Can take a filename, url or file
        like object and always returns
        a file like object)
        """
        fis = fn or fo
        closeAfterUse = 0
        
        # check input spec
        if not hasattr(fis, 'read'):
            # must be a string
            closeAfterUse = 1
            
            # check for url
            if fis.find("://") > 0:
                # url found
                fis = urllib2.urlopen(fis)
            else:
                # file system
                fis = open(fis,"rb")
                
        return fis,closeAfterUse
                
    def createNewDocument(self, name):
        """
        Initializes a pxb and sets up the root node etc.  You
        have to pass in a name, namespaceURI and doctype are 
        optional.
        """
        
        self._dom = etree.Element(name)
        self._setup()        
        self._setFileName = None
        
        return self
    
    # 
    # -- Bean Methods --
    #
    def getNodeAttributes(self):
        "get the current nodes attributes as a dict"
        return self._extractAttrsToDict(node=self._currNode)
    
    def hasAttribute(self, attrName):
        "test for existance of attribute in current node"
        
        # return node response
        return attrName in self._currNade.keys()
    
    def hasChildNode(self, chName):
        rs = False
        
        if len(self._getNodesByName("./%s" % chName)) > 0:
            rs = True
            
        return rs
    
    def getNodeAttrValue(self, attrName):
        """get an attribute value from the current node"""
        
        # return the attribute value
        return self._node.get(attrName)
        
    def setNodeAttrValue(self, attrName, attrValue):
        """
        Sets an attribute for the current node and 
        returns self (pxb) for chaining
        """
        self._node.set(attrName, attrValue)
        return self

    def addChildNode(self, name, content=None, attrs={}, append=1):
        """add new child node to pxb structure. inserts
        as the first child if append=0 else appends to
        the end of child list"""
        # create tag
        node = etree.Element(name,attrs)
        
        # add content to tag
        node.text = content
            
        if append:
            self._currNode.append(node)           
        else:
            self._currNode.insert(0,node)
            
        return self._makePXB(node)
        
    def removeChildNode(self, child):
        """
        Removes a child from the current node and
        returns self for chaining.
        """
        self._currNode.remove(child.getNode())
        return self
       
    def getNodeValue(self, nodeName='.', context=None):
        """Generalized method to extract text data from DOM"""
        # set the context
        if context == None:
            context = self._currNode

        # find the text nodes for the name
        byName = '%s/text()' % nodeName  

        # get the node in list (if any)
        nodes = self._getNodesByName(byName, context)
        rv = ''
        if nodes == None:
            return None
        
        for node in nodes:
            rv += node.text.strip()
            
        # and return the text within
        return rv

    def setNodeValue(self, nodeName, nodeValue, context=None, save=0):
        """Update the text value of a node set"""
        byName = '%s/text()' % nodeName
        nodes = self._getNodesByName(byName, context)
        
        # update the value 
        for node in nodes:
            node.text = nodeValue
                
        # check persistance flag        
        if save == 1:
            self.save()
    
        return self
    
    def setNode(self, node, dom=None):
        """Rebuild with new node"""
        self._node = node
        self._dom = dom
        self._setup()
        
    def getNodeName(self):    
        """Retrieve the current node's name"""
        return self._currNode.tag
        
    def getNode(self):
        """Return the underlying  node"""
        return self._node
        
    def getDOM(self):
        """Retrieve a reference to the underlying DOM"""
        return self._dom
    
    def setDOM(self, dom):
        """Reset this PBX and use the specified DOM"""
        self._node = None
        self._dom = dom
        self._setup()
    
    def getFileName(self):
        """Retrieve the current file name"""
        return self._fileName

    def setFileName(self, fileName):
        """Set the file name for future save / load"""
        rv = self._fileName
        self._fileName = fileName
        return rv
    
    def getPXB(self, nodeName=None, context=None):
        """Return PXB structure from a sub node"""

        p = None
        
        if context == None:
            context = self._currNode
        node = None
        try:
            if nodeName != None:
                # find the node
                node = self._getNodesByName(nodeName, context)[0]
                
            else:
                # use the current node
                node = self._currNode

            # set pbx/node linkage
            p = self._makePXB(node)
            

        except Exception:
            p = None

        # return the results
        return p
            
    def createQuery(self, nodeName="", attributes={}):
        """Utility method for creating pxbQuery objects"""
        xq = pxbQuery(nodeName, attributes)
        xq._setpxb(self)
        
        return xq
    
    def select(self, query):
        """Use pxbQuery to find nodes in sub-node tree"""
        # get list of nodes that match the name
        nodeList = self._getNodesByName(query.getNodeName())

        # prepare output buffer
        rSet = pxbResultSet()
        
        # extract query attributes
        attrs = query.getAttributes()
            
        # then walk the node list 
        for node in nodeList:
            # set include flag
            inc = 1

            # test for existence and equality
            for key in attrs.keys():
                try:
                    # test node attribute against query
                    tstN = str(node.getAttribute(key))
                    tstQ = str(attrs[key])
                    
                    if tstN != tstQ:
                        # dis-include node
                        inc = 0
                        
                        break
                except:
                    # attribute is non-existing
                    inc = 0

                    sys.stderr.write("Error testing %s" % key)
                    
                    # discontinue query match 
                    break
                        
            # if all tests pass,
            if inc == 1:
                # add to list of baby PXBs
                rSet.addNode(self._makePXB(node))
                
        # set and return the results
        rSet.rewind()
        return rSet


    # 
    # -- Persistence Methods --
    #
    def load(self, fil=None, fileObj=None):
        """Create the internal dom from a file or
        preopened stream.  If the fileObj is passed
        it will be used, else a stream will be 
        created from either the fileName parameter,
        the _fileName attribute or stdin (in that 
        order).
        """
        # filename, url or file like object
        insrc,closeFile = self._setupInputSource(fil,fileObj)
        if fil:
            self._fileName = fil
        try :
            self._dom = etree.parse(insrc)

            # clean up
            if closeFile == 1:
                insrc.close()

            # reset 
            self._node = None
            self._setup()
            
            return self
            
        except Exception, e:
            # mark self as invalid
            self._validity = None
            self._dom = None
            self._filename = ""
            self._node = None
            self._currNode = None
            self._errMsg = str(e)
            n = Notification(self._errMsg)
            n.setException(e)
            raise Exception, e    
           
    def save(self, fileName=None, fileObj=None,**args):
        """Write the current dom to a file (or a
        file like object). use Standard Out.
        """
        # use best guess and pass it to lxml
        fileOut = fileName or fileObj or self._fileName or sys.stdout
         
        self.dom.write(fileOut,args)
        
                    
    def transform(self, xslts):
        """
        Wrapper for XSLT Transformations from  that will
        transform current state using specified list of XSLT 
        stylesheets using local file system or HTTP protocol.
        
        Note: even if using single style sheet, send in list.
        """
        # list or single sheet
        xlist = xslts
        if len(xslts[0]) == 1:
            # its a string (possibly comma separated list)
            xlist = xslts.split(",")            
       
        # load up the stylesheets
        tr = self._dom
        for xslt in xlist:
                
            ins = self._setupInputSource(xslt)
            xf = etree.parse(ins)
            tr = tr.xslt(xf)
        
        return self._makePXB(tr)
        
    def dump(self, fileObj=None):
        '''Dump XML file to file object or stdout'''
        fout = fileObj or sys.stdout
        fout.write(self.toString())

    def toString(self):
        """Dump to string and return"""
        # need string io for file like obje
        return etree.tostring(self._dom, pretty_print=True)
    
if __name__ == "__main":
    # go team go    
    p = pxb(sys.argv[1])
    p.dump()
